当前位置:PHP

原文链接

原文作者:ANANT GARG

什么是MVC?

维基百科的解释是:

模型-视图-控制器(MVC)是软件工程中的一种架构模式。正确使用这种方式可以使业务逻辑从用户接口中分离出来,这样做的好处是应用的显示界面或者业务逻辑容易修改,同时不影响其它地方。在MVC中,Model表示的是应用的信息(数据);View对应于用户界面的元素(如:text,checkbox items 等html元素);Controller通过管理数据通信和业务规则来和Model进行信息交互。

简单来说:

  1. Model 处理所有的数据库逻辑,并为我们提供一个数据库连接抽象层。
  2. Controller 代表我们的业务逻辑。换言之,就是所有的if 和else这样的判断语句。
  3. View 表示我们的显示逻辑。即网站中的 HTML/XML/JSON代码文件。

为什么要写一个我们自己的MVC框架呢?

这篇教程并不是一个满足你的框架需求的完整的PHP MVC解决方案。实际上,已经有很多优秀的MVC框架了。

那么为什么我们要写一个自己的框架呢?首先,这是一种很好的学习方法。从中你可以深入的学习PHP这门语言,你也可以学习到面向对象的编程知识,设计模式和思考方法。更重要的是,你可以完全控制你的框架。你可以把自己的想法融入到自己的框架中。

你可以使用你喜欢的代码、函数及模块规范,看你喜好啦!

让我们开始吧…

目录结构

尽管上面有的目录内并没有内容,但是考虑到将来可能的扩展,建议还是保留这些空目录。下面解释下每个目录的用途:

application - 用于存放应用代码

config - 用于存放数据库/服务器的配置信息

db - 存放数据库备份文件

library - 存放框架代码

public - 存放 js代码、css文件以及图像和动画文件

scripts - 存放命令行工具

tmp - 存放错误日志、访问日志等临时文件

目录结构已经准备好了,接下来要做一些代码约定:

代码约定:

  1. 数据库表名应该由有意义的小写英文单词构成,且为复数形式。如 items,cars。
  2. 模块名为首字母大写且为单数的英文单词。如:Item,Car。
  3. 控制器名由模块名加”s”再加上“Controller”后缀组成。如:ItemsController。
  4. 视图目录名为复数,目录内包含的文件,其名字为该视图所包含的动作的名字。如: items/view.php,cars/buy.php。

下面正式开始了…

首先,在根目录下放置一个.htaccess文件。用于将访问请求重定向到public目录。.htaccess文件内容如下:

<IfModule mod_rewrite.c>
    RewriteEngine on
    RewriteRule    ^$    public/    [L]
    RewriteRule    (.*) public/$1    [L]
 </IfModule>

public目录内也放置一个.htaccess文件。用于将所有的请求重定向到index.php文件。.htaccess文件内容如下:

<IfModule mod_rewrite.c>
RewriteEngine On 
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php?url=$1 [PT,L]
</IfModule>

解释如下:如果请求访问的地址不是网站中存在的文件或目录,则将其请求重定向到index.php?url=PATHNAME

例如:你访问的地址是http://localhost/items/viewall 。则REQUEST_FILENAME的值就是/items/viewall。但是网站根目录下并没有/items/viewall这个目录,于是重定向规则就产生作用了。将/items/viewall作为参数值赋给url,以便控制器截获请求进行处理。

使用重定向有许多好处:

  1. 我们可以使用它来自启动。换句话说,就是将除image文件、js文件和css文件以外的所有访问请求全部重定向到index.php文件。
  2. 我们可以使用美观并且seo友好的URL。
  3. 我们可以有单一的访问入口。

现在我们添加一个index.php文件到我们的public目录中

<?php   
/*定义目录分割符*/
define('DS', DIRECTORY_SEPARATOR);
/*定义根目录*/
define('ROOT', dirname(dirname(__FILE__)));
/*获取请求参数*/
$url = $_GET['url'];
require_once (ROOT . DS . 'library' . DS . 'bootstrap.php');

注意,上面的文件中并没有包含php的结束标记?>,之所以这样做是为了避免在我们的输出中被注入额外的空白。在我们的index.php文件中设置了$url变量,并且导入了位于library目录内的bootstrap.php文件。

下面看一下bootstrap.php文件的内容

<?php
require_once (ROOT . DS . 'config' . DS . 'config.php');
require_once (ROOT . DS . 'library' . DS . 'shared.php');

很明显,bootstrap.php文件的内容可以直接包含在index.php文件内。但是如果这样做将无法满足未来可能的扩展需求了。

接下来看一下shared.php文件的内容,这个文件里将做一些实际的工作了。

<?php

/** 检查当前是否是在开发环境。如果在开发环境中,则允许在网页中显示运行错误信息 **/
function setReporting() {
if (DEVELOPMENT_ENVIRONMENT == true) {
    error_reporting(E_ALL);
    ini_set('display_errors','On');
} else {
    error_reporting(E_ALL);
    ini_set('display_errors','Off');
    ini_set('log_errors', 'On');
    ini_set('error_log', ROOT.DS.'tmp'.DS.'logs'.DS.'error.log');
}
}
/** 检查魔术引用标记并移除这些标记 **/
function stripSlashesDeep($value) {
    $value = is_array($value) ? array_map('stripSlashesDeep', $value) : stripslashes($value);
    return $value;
}

function removeMagicQuotes() {
if ( get_magic_quotes_gpc() ) {
    $_GET    = stripSlashesDeep($_GET   );
    $_POST   = stripSlashesDeep($_POST  );
    $_COOKIE = stripSlashesDeep($_COOKIE);
}
}
/** 检查register 全局变量并移除 **/
function unregisterGlobals() {
    if (ini_get('register_globals')) {
        $array = array('_SESSION', '_POST', '_GET', '_COOKIE', '_REQUEST', '_SERVER', '_ENV', '_FILES');
        foreach ($array as $value) {
            foreach ($GLOBALS[$value] as $key => $var) {
                if ($var === $GLOBALS[$key]) {
                    unset($GLOBALS[$key]);
                }
            }
        }
    }
}

/**主功能函数 **/
function callHook() {
    global $url;
    $urlArray = array();
    $urlArray = explode("/",$url);
    $controller = $urlArray[0];
    array_shift($urlArray);
    $action = $urlArray[0];
    array_shift($urlArray);
    $queryString = $urlArray;
    $controllerName = $controller;
    $controller = ucwords($controller);
    $model = rtrim($controller, 's');
    $controller .= 'Controller';
    $dispatch = new $controller($model,$controllerName,$action);

    if ((int)method_exists($controller, $action)) {
        call_user_func_array(array($dispatch,$action),$queryString);
    } else {
        /* Error Generation Code Here */
    }
}
/** 自动加载需要的类 **/
function __autoload($className) {
    if (file_exists(ROOT . DS . 'library' . DS . strtolower($className) . '.class.php')) {
        require_once(ROOT . DS . 'library' . DS . strtolower($className) . '.class.php');
    } else if (file_exists(ROOT . DS . 'application' . DS . 'controllers' . DS . strtolower($className) . '.php')) {
        require_once(ROOT . DS . 'application' . DS . 'controllers' . DS . strtolower($className) . '.php');
    } else if (file_exists(ROOT . DS . 'application' . DS . 'models' . DS . strtolower($className) . '.php')) {
        require_once(ROOT . DS . 'application' . DS . 'models' . DS . strtolower($className) . '.php');
    } else {
        /* Error Generation Code Here */
    }
}

setReporting();
removeMagicQuotes();
unregisterGlobals();
callHook();

下面来详细地解释下上面的代码。setReporting()函数在常量DEVELOPMENT_ENVIRONMENTtrue时用于显示错误信息。unregisterGlobals()函数和removeMagicQuotes()函数用于移除注册全局变量和魔术引用。__autoload()这个魔术函数可以帮助我们自动加载我们需要的类,calllHock()函数则是主进程函数。

译者的话:

为什么要移除注册全局变量呢?首先,我们要知道什么是注册全局变量。举例来说,如果PHP 配置文件中 PHP 指令register_globals = on,那么,通过$_GET, $_POST $_SESSION $_COOKIE $_REQUEST $_SERVER $_ENV $_FILES这样的超级全局变量传递的参数将会成为全局变量,这样的全局变量的值有可能会覆盖你的php脚本文件中明确定义的变量的值,并由此带来不安全的后果。下面是一个错误使用register_globals = on的例子,该示例来源于PHP官网

<?php
// 当用户合法的时候,赋值 $authorized = true
if (authenticated_user()) {
    $authorized = true;
}
// 由于并没有事先把 $authorized 初始化为 false,
/* 当 register_globals 打开时,如果攻击者在浏览器中输入 http://localhost/auth.php?authorized=1 进行访问的话,将直接进入到你的管理后台,而不需要任何账号和密码。因为这种方式下,$_GET['authorized']这个关联数组的键authorized便是注册全局变量,
直接将该键的值设为1,那么下面的if条件中的$authorized的值便是1了。那么任何人都可以通过这样的方式绕过身份验证*/
if ($authorized) {
    include "/highly/sensitive/data.php";
}
?>

那么,为什么要移除魔术引用呢?同样,我们要知道什么是魔术引用。当我们通过url传参时,以http://localhost/index.php?name="\zhangsan"为例。由于url参数name的参数值中含有\这样的特殊字符,为了能正确的将\zhangsan这样的带特殊符号的参数值赋值给变量name。当get_magic_quotes_gpc()的值为true时,php内部会自动将\zhangsan这个参数值转换为\\zhangsan。所以为了避免这些自动加入的转换符号(如:\)污染参数值,在share.php文件中定义了removeMagicQuotes()函数和stripSlashesDeep()函数来移除多余的字符(如\等)。以此保证输出的参数值始终是\zhangsan,而不是\\zhangsan。需要特别说明的是,从php 5.4.0版本开始,魔术引用功能已经从PHP中移除了,所以get_magic_quotes_gpc()的值始终为FALSE。换句话说,如果你的服务器中安装的php版本是5.4.0及以上版本的话,在share.php文件中也可以不定义removeMagicQuotes()stripSlashesDeep()这两个函数。当然了,为了兼容低版本的php,还是保留这两个函数的好。

下面先让我说明下我们这个MVC框架的URL,它看起来像这样 - yoursite.com/controllerName/actionName/queryString

callHook()函数从index.php文件中的$url变量获取到实际的URL值,然后将该值分割,分别作为变量$controller$action$queryString的值。其中$model的值为$controller值的单数形式。

例如,如果我们的URL是 localhost/items/view/1/get-milk,那么 Controller就是items,Model就是item(与mysql表数据同步),Action是view,Query String是一个数组(1,get-milk)

当将URL的值分割完毕,之后开始新创建一个 $controller 类的控制器对象,同时调用该类的 $action方法。

现在让我们创建一个控制器文件controller.class.php作为我们所有控制器文件的基类,一个model.class.php文件作为所有模板文件的基类

controller.class.php

<?php
class Controller {
    protected $_model;
    protected $_controller;
    protected $_action;
    protected $_template;
    function __construct($model, $controller, $action) {

        $this->_controller = $controller;
        $this->_action = $action;
        $this->_model = $model;

        $this->$model =new $model;
        $this->_template =new Template($controller,$action);

    }
    function set($name,$value) {
        $this->_template->set($name,$value);
    }

    function __destruct() {
            $this->_template->render();
    }

}

该类用于controller,model和view(template class)之间的所有通信,它创建了一个model类对象和一个template类对象。model类对象的名字就是该model类的名字,所以我们可以通过我们的controller直接使用诸如$this->Item->selectAll()这样的方法。

当我们销毁该类时我们通过调用render()方法来显示我们的view(template)文件。

model.class.php

<?php
class Model extends SQLQuery {
    protected $_model;
    function __construct() {

        $this->connect(DB_HOST,DB_USER,DB_PASSWORD,DB_NAME);
        $this->_model = get_class($this);
        $this->_table = strtolower($this->_model)."s";
    }

    function __destruct() {
    }
}

该类继承自SQLQuery类,SQLQuery类是mysql连接的一个抽象层。根据你的实际需要,你也可以定义其它的数据库连接类。

SQLQuery.class.php

<?php

class SQLQuery {
    protected $_dbHandle;
    protected $_result;
    /** Connects to database **/
    function connect($address, $account, $pwd, $name) {
        $this->_dbHandle = @mysqli_connect($address, $account, $pwd);
        if ($this->_dbHandle) {
            if (mysqli_select_db($this->_dbHandle,$name)) {
                return 1;
            }
            else {
                return 0;
            }
        }
        else {
            return 0;
        }
    }
    /** Disconnects from database **/
    function disconnect() {
        if (@mysqli_close($this->_dbHandle) != 0) {
            return 1;
        }  else {
            return 0;
        }
    }    
    function selectAll() {
        $query = 'select * from `'.$this->_table.'`';
        return $this->query($query);
    }    
    function select($id) {
        $query = 'select * from `'.$this->_table.'` where `id` = \''.mysqli_real_escape_string($this->_dbHandle,$id).'\'';
        return $this->query($query, 1);    
    }   
    /** Custom SQL Query **/
    function query($query, $singleResult = 0) {
        $this->_result = mysqli_query($this->_dbHandle,$query);
        if (preg_match("/select/i",$query)) {
        $result = array();
        $table = array();
        $field = array();
        $tempResults = array();
        $numOfFields = mysqli_num_fields($this->_result);
        for ($i = 0; $i < $numOfFields; ++$i) {
            mysqli_field_seek($this->_result,$i);
            $fieldinfo=mysqli_fetch_field($this->_result);
            array_push($table,$fieldinfo->table);
            array_push($field,$fieldinfo->name);
        }
            while ($row = mysqli_fetch_row($this->_result)) {
                for ($i = 0;$i < $numOfFields; ++$i) {
                    $table[$i] = trim(ucfirst($table[$i]),"s");
                    $tempResults[$table[$i]][$field[$i]] = $row[$i];
                }
                if ($singleResult == 1) {
                    mysqli_free_result($this->_result);
                    return $tempResults;
                }
                array_push($result,$tempResults);
            }
            mysqli_free_result($this->_result);
            return($result);
        }

    }

    /** Get number of rows **/
    function getNumRows() {
        return mysqli_num_rows($this->_result);
    }

    /** Free resources allocated by a query **/

    function freeResult() {
        mysqli_free_result($this->_result);
    }

    /** Get error string **/

    function getError() {
        return mysqli_error($this->_dbHandle);
    }
  /* connHandle函数为译者所加,目的是为了itemscontroller.php文件中mysqli_real_escape_string函数可以获得数据库连接句柄。*/
    function connHandle(){
        return $this->_dbHandle;
    }
}

注: 该文件译者略有改动,因为译者开发环境PHP版本为7.0,已经不支持原文中mysql_connect这样的数据库连接函数。如果你的环境中php属于5及以前的版本,可参照原文中的SQLQuery.class.php文件。

SQLQuery.class.php文件是整个MVC框架的核心。为什么这样说呢?显而易见,正是因为其创建的SQL抽象层,使我们减少了许多编程工作。在下一篇教程中我们将在SQLQuery.class.php加入一些高级功能。现在,我们暂且让它简单点儿…

connect()disconnect()函数的功能很明显,这里就不再赘述。我要特别讲讲query()这个方法。假设我们的SQL查询语句像下面这样:

SELECT table1.field1 , table1.field2, table2.field3, table2.field4 FROM table1,table2 WHERE ….

那么问题来了,我们的脚本要怎样找出所有的输出域和它们要进行同步的数据库表然后再把他们放置到数组中,同时 $field$table 还要保持原来的索引值呢?以我们上面的查询语句来说,$table$field看起来像这样:

$field = array(field1,field2,field3,field4);
$table = array(table1,table1,table2,table2);

脚本文件获取所有的行,将数据库表名转换成Model名字(即移除表名末尾的字符s,并将表名的首字母变为大写)并放到我们的多维数组中,然后返回结果。返回的结果格式如$var[‘modelName’][‘fieldName’]。这样的输出格式可以让我们更容易的在我们的视图中输出数据库元素。

template.class.php

<?php
class Template {
    protected $variables = array();
    protected $_controller;
    protected $_action;
    function __construct($controller,$action) {
        $this->_controller = $controller;
        $this->_action = $action;
    }
    /** Set Variables **/

    function set($name,$value) {
        $this->variables[$name] = $value;
    }
    /** Display Template **/

    function render() {
        extract($this->variables);

            if (file_exists(ROOT . DS . 'application' . DS . 'views' . DS . $this->_controller . DS . 'header.php')) {
                include (ROOT . DS . 'application' . DS . 'views' . DS . $this->_controller . DS . 'header.php');
            } else {
                include (ROOT . DS . 'application' . DS . 'views' . DS . 'header.php');
            }

        include (ROOT . DS . 'application' . DS . 'views' . DS . $this->_controller . DS . $this->_action . '.php');         

            if (file_exists(ROOT . DS . 'application' . DS . 'views' . DS . $this->_controller . DS . 'footer.php')) {
                include (ROOT . DS . 'application' . DS . 'views' . DS . $this->_controller . DS . 'footer.php');
            } else {
                include (ROOT . DS . 'application' . DS . 'views' . DS . 'footer.php');
            }
    }

}

以上的代码很容易看懂。有一点需要指出的是 - 如果在 view/controllerName 目录内没有找到 header.php 和 footer.php 文件,那么将加载位于 view 目录内的公共 header.php和footer.php文件。

现在可以在config目录中添加一个 config.php文件,然后创建我们的第一个 model,view和controller

config.php

<?php
/** Configuration Variables **/
define ('DEVELOPMENT_ENVIRONMENT',true);
define('DB_NAME', 'project');
define('DB_USER', 'yourusername');
define('DB_PASSWORD', 'yourpassword');
define('DB_HOST', 'localhost');

创建数据库project和表items,并插入数据。

CREATE DATABASE project;
USE project;
CREATE TABLE `items` (
  `id` int(11) NOT NULL auto_increment,
  `item_name` varchar(255) NOT NULL,
  PRIMARY KEY  (`id`)
);

INSERT INTO `items` VALUES(1, 'Get Milk');
INSERT INTO `items` VALUES(2, 'Buy Application');

创建模型文件 item.php

<?php
class Item extends Model {
}

在本篇教程中,Item类中直接继承Model中的属性和方法,在下一篇教程中,Item类中将会有更多的信息。

创建控制器文件itemscontroller.php

<?php
class ItemsController extends Controller {
    function view($id = null,$name = null) {

        $this->set('title',$name.' - My Todo List App');
        $this->set('todo',$this->Item->select($id));
    }
    function viewall() {

        $this->set('title','All Items - My Todo List App');
        $this->set('todo',$this->Item->selectAll());
    }
    function add() {
        $todo = $_POST['todo'];
        $this->set('title','Success - My Todo List App');
        $this->set('todo',$this->Item->query('insert into items (item_name) values (\''.mysqli_real_escape_string($this->Item->connHandle(),$todo).'\')')); 
    }
    function delete($id = null) {
        $this->set('title','Success - My Todo List App');
        $this->set('todo',$this->Item->query('delete from items where id = \''.mysqli_real_escape_string($this->Item->connHandle(),$id).'\'')); 
    }
}

最后在views目录下创建一个名字为items的子目录,并将下面的视图文件放到items目录下:

view.php

<h2><?php echo $todo['Item']['item_name']?></h2>
    <a class="big" href="../../../items/delete/<?php echo $todo['Item']['id']?>">
    <span class="item">
    Delete this item
    </span>
    </a><br/>

viewall.php

<form action="../items/add" method="post">
<input type="text" value="I have to..." onclick="this.value=''" name="todo"> <input type="submit" value="add">
</form>
<br/><br/>
<?php $number = 0?>
<?php foreach ($todo as $todoitem):?>
    <a class="big" href="../items/view/<?php echo $todoitem['Item']['id']?>/<?php echo strtolower(str_replace(" ","-",$todoitem['Item']['item_name']))?>">
    <span class="item">
    <?php echo ++$number?>
    <?php echo $todoitem['Item']['item_name']?>
    </span>
    </a><br/>
<?php endforeach?>

add.php

<a class="big" href="../items/viewall">Todo successfully added. Click here to go back.</a><br/>

delete.php

<a class="big" href="../../items/viewall">Todo successfully deleted. Click here to go back.</a><br/>

header.php

<html>
<head>
<title><?php echo $title?></title>
<style>
.item {
width:400px;

}
input {
    color:#222222;
font-family:georgia,times;
font-size:24px;
font-weight:normal;
line-height:1.2em;
    color:black;
}

 a {
    color:#222222;
font-family:georgia,times;
font-size:24px;
font-weight:normal;
line-height:1.2em;
    color:black;
    text-decoration:none;

}

a:hover {
    background-color:#BCFC3D;
}
h1 {
color:#000000;
font-size:41px;
letter-spacing:-2px;
line-height:1em;
font-family:helvetica,arial,sans-serif;
border-bottom:1px dotted #cccccc;
}

h2 {
color:#000000;
font-size:34px;
letter-spacing:-2px;
line-height:1em;
font-family:helvetica,arial,sans-serif;

}
</style>
</head>
<body>
<h1>My Todo-List App</h1>

footer.php

</body>
</html>

好了,一切完成后,让我们来测试下我们的框架吧。在你的浏览器中输入http://localhost/items/viewall ,看看会输出什么吧…

评论
一些有趣的事儿
叽里咕噜 2019-05-29 16:51:38
meixian
热爱 2021-11-02 21:23:41
回复 叽里咕噜: hi
热爱 2021-11-20 17:07:59
回复 叽里咕噜: 啥?